package contegor.scire.structure.lang;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ExecutionException;

import contegor.scire.core.Action;
import contegor.scire.core.Concept;
import contegor.scire.core.Id;
import contegor.scire.core.Link;
import contegor.scire.core.Link.Flow;
import contegor.scire.core.MissingElementException;
import contegor.scire.core.queries.GetConcepts;
import contegor.scire.structure.Constants;
import contegor.scire.structure.Context;

/**
 * Given the {@link Id} of an {@link Action} present in it's {@link Context}, this class will locate that
 * {@link Action} and will execute it, loading it's {@link Action#getPlugIn()} and calling the plug-in's
 * execution {@link Action#getImplementation()}. The results will be left in the same {@link Context}.
 * Commit this changes is outside this class responsibility.
 * 
 * @author  Héctor Cirbián Pousa
 */
public class ActionExecutor {

  private Context context;

  public ActionExecutor(Context context) {
    this.context = context;
  }

  public void setContext(Context context) {
    this.context = context;
  }

  public Context getContext() {
    return this.context;
  }

  /**
   * Execute the {@link Action} identified by the given {@link Id}. Loads it's {@link Action#getPlugIn()}
   * and calls the plug-in's execution {@link Action#getImplementation()}. The results will be left in the
   * same {@link Context}. Commit this changes is outside this class responsibility.
   * 
   * The action parameters have to be pointed previously in the context, with links from the 'action'-'requirement'
   * link to the 'parameter' concept. This can be done with persistent links, for permanent parameters, or with
   * transient links for one execution parameters.
   * 
   * This method allows non 'scire' parameters, as java additional arguments. Use with care.
   * 
   * @param actionId {@link Id} identifying the action to be executed.
   * @return Object with action's result, if there is one.
   * @throws InterruptedException
   * @throws ExecutionException
   * @throws MissingElementException
   * @throws ClassNotFoundException
   * @throws SecurityException
   * @throws NoSuchMethodException
   * @throws IllegalArgumentException
   * @throws IllegalAccessException
   * @throws InvocationTargetException
   */
  public Object execute(Id actionId, Object ... args) throws InterruptedException, ExecutionException, MissingElementException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
    GetConcepts getAction = new GetConcepts(actionId.getParentId(), actionId.getLastIdFragment());
    List<Concept> result = this.context.process(getAction).get();
    if ((result == null) || (result.size() == 0) || !(result.get(0) instanceof Action))
      result = this.context.pull(getAction);
    if ((result == null) || (result.size() == 0))
      throw new MissingElementException("Action " + actionId + " not found.");
    if (!(result.get(0) instanceof Action))
      throw new MissingElementException(actionId + " is not an Action.");
    Action action = (Action)result.get(0);
    // Check if requirements are met.
    for (Link link: action.getLinks().values()) {
      if (link.isLinkedWith(Constants.TO_REQUIRE) && (Flow.DOWNSTREAM == link.getFlow())) {
        // This is a requirement the action needs. Check if it's met.
        Link parameters = link.getLink(Constants.PARAMETER);
        if ((parameters == null) || (parameters.getLinksCount() <= 0))
          throw new MissingElementException("Missing requirement:" + link.getTarget());        
      }
    }
    // Load Action's plug-in & implementation, and call it.
    Class<?> plugin = Class.forName(action.getPlugIn());
    if ((args != null) && (args.length > 0)) {
      Object[] aux = new Object[args.length + 2];
      aux[0] = this.context;
      aux[1] = action;
      for (int i=2; i<aux.length; i++)
        aux[i] = args[i-2];
      Class<?>[] types = new Class<?>[aux.length];
      for (int i=0; i<aux.length; i++)
        types[i] = aux[i].getClass();
      String methodName = action.getImplementation();
      Method found = null;
      external:
      for (Method method: plugin.getMethods()) {
        if (method.getName().endsWith(methodName)) {
          Class<?>[] parameterTypes = method.getParameterTypes();
          if (parameterTypes.length == types.length) {
            for (int i=0; i<types.length; i++)
              if (!parameterTypes[i].isAssignableFrom(types[i]))
                continue external;
            found = method;
            break;
          }
        }
      }
      if (found == null)
        new NoSuchMethodException("PlugIn: " + action.getPlugIn() + "#" + action.getImplementation() + " doesn't exists.");
      return found.invoke(null, aux);
    }
    else {
      Method impl = plugin.getMethod(action.getImplementation(), Context.class, Action.class);
      return impl.invoke(null, this.context, action);
    }
  }
}
